<?php
/**
 ***********************************************************************************************************************
 * 访问控制系统
 * @author llq
 ***********************************************************************************************************************
 */
namespace soa\firewall;

use artisan\cache;
use artisan\config;
use artisan\input;
use firewall\lib\access;
use firewall\lib\rule;


/**
 * Class firewall
 * @package soa\firewall
 */
class firewall
{
    /**
     * @var array 访问控制配置 包括基础访问规则
     */
    private $config;

    /**
     * @var array 后台自定义的访问规则
     */
    private $rule;

    /**
     * @var string 客户端（合作伙伴名称或app_id） 使用者可以传入，否则自己获取(默认使用浏览器useragent)
     */
    private $client;

    /**
     * @var string 当前服务名称 由使用者传入
     */
    private $service;

    /**
     * @var string 当前IP 使用者可以传入，否则自己获取input::ip()
     */
    private $ip;

    /**
     * @var array 当前用户访问记录
     */
    private $record;

    /**
     * @var self 单例模式自实例化 方便使用
     */
    static $instance;


    /**
     * 启动一个访问控制实例
     * @param $service_name
     * @param $client_id string 可以是app_id或者parter_name，合作伙伴账号等
     * @return bool
     */
    public static function run($service, $client = '', $ip = '')
    {
        return self::getInstance()->check($service, $client, $ip);
    }

    /**
     * 访问控制检测
     * @return bool
     */
    private function check($service, $client, $ip)
    {
        $this->service = $service;
        $this->client = $client;
        $this->ip = $ip;

        // 规则检查 注：当前访问地址是否在防火墙控制之内，如果不是，跳过控制
        if(!$this->configCheck()) {
            return true;
        }

        // 无法获知客户端信息的访问检测 注：如果配置中设置这些客户无法访问则会屏蔽访问
        if(!$this->unknowClientCheck()) {
            $this->render('unknow');
        }

        // 访问控制
        if(!$this->ruleCheck()) {
            // 记录当前访问者信息
            $this->addDanger($this->getIp(), $this->record);
            // 输出频率受限信息
            $this->render('limit');
        }

        // 检查合法 通过访问
        return true;
    }

    /**
     * 规则检测
     * @param $service_name
     * @return bool
     */
    private function configCheck()
    {
        // 不合法服务跳过控制 跳过
        if(empty($this->service) || !is_string($this->service)) {
            return false;
        }

        // 访问规则控制配置不合法 跳过
        $conf = config::get('firewall');
        if(
            empty($conf[$this->service]) ||
            empty($conf[$this->service]['enable']) ||
            empty($conf[$this->service]['rate'])
        ) {
            return false;
        }

        $this->config = $conf[$this->service];

        return true;
    }

    /**
     * 无法获知客户端信息的访问检测
     * @return bool
     */
    private function unknowClientCheck()
    {
        if(!empty($this->rule['block_unknow_ip']) && (input::ip() == 'unknow' || input::ip() == '')) {
            return false;
        }
        if(!empty($this->rule['block_unknow_useragent']) && (input::userAgent() == 'unknow' || input::userAgent() == '')) {
            return false;
        }

        return true;
    }

    /**
     * 黑名单/白名单控制规则为全局的 注:如果当前IP在黑名单内,则当前IP无法访问快宝所有服务
     * @return bool
     */
    private function ruleCheck()
    {
        $list = $this->getRuleInstance()->getRules($this->getIp(), $this->getClient());

        // 为空跳过
        if(empty($list)) {
            return true;
        }

        // 黑名单
        if($list['blackwhite_ip'] == 'black' || $list['blackwhite_ip_segment'] == 'black') {

            $this->render('black');
        }

        // 白名单
        if($list['blackwhite_ip'] == 'white' || $list['blackwhite_ip_segment'] == 'white') {
            return true;
        }

        // 添加 并且获取该用户的访问记录
        $this->record = $this->getAccessInstance()->add($this->getIp(), $this->getClient());

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // 存在后台自定义的IP频率限制，或者客户端频率限制，按后台的来
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        //myprint($list);
        // 按后台定义的该IP允许的频率
        if(!empty($list['ip_limit'])) {
            if(!empty($list['ip_limit']['daily']) && $list['ip_limit']['daily'] < $this->record['daily']) {
                return false;
            }
            if(!empty($list['ip_limit']['hourly']) && $list['ip_limit']['hourly'] < $this->record['hourly']) {
                return false;
            }
            return true;
        }

        // 按后台定义的该客户端（合作伙伴账号）允许的频率
        if(!empty($list['client_limit'])) {
            if(!empty($list['client_limit']['daily']) && $list['client_limit']['daily'] < $this->record['daily']) {
                return false;
            }
            if(!empty($list['client_limit']['hourly']) && $list['client_limit']['hourly'] < $this->record['hourly']) {
                return false;
            }
            return true;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        /// 否则按默认配置的频率进行控制
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        // 检测当前天是否已经超过频率
        if($this->config['rate']['daily'] <= $this->getRateByWeight($this->record['daily'], $this->record[$this->getClient() . '_daily'])) {
            return false;
        }

        // 检测当前小时是否已经超过频率
        if($this->config['rate']['hourly'] <= $this->getRateByWeight($this->record['hourly'], $this->record[$this->getClient() . '_hourly'])) {
            return false;
        }


        return true;
    }


    /**
     * 记录危险IP
     * @param $ip
     * @param $data
     * @return bool
     */
    private function addDanger($ip, $data)
    {
        return $this->getAccessInstance()->addDanger($ip, $data);
    }


    /**
     * 获取客户端标识，只是防误杀的干扰作用
     *
     * $_SERVER信息 不同浏览器、不同浏览器版本、不同系统版本、不同系统、不同的客户端语言等等都会不同
     *
     * 如果单就一个公司内部所有用户来说，还是有一部分所有的都相同的，咱不考虑，有干扰因素总比没有好
     *
     * @return string
     */
    private function getClient()
    {
        if(!(empty($this->client))) {
            return $this->client;
        }
        $accept = empty($_SERVER['HTTP_ACCEPT']) ? '' : $_SERVER['HTTP_ACCEPT'];
        $useragent = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
        $encode = empty($_SERVER['HTTP_ACCEPT_ENCODING']) ? '' : $_SERVER['HTTP_ACCEPT_ENCODING'];
        $lang = empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? '' : $_SERVER['HTTP_ACCEPT_LANGUAGE'];
        return md5($accept . $useragent . $encode . $lang);
    }

    /**
     * 获取IP
     * @return string
     */
    private function getIp()
    {
        return empty($this->ip) ? input::ip() : $this->ip;
    }


    /**
     * 获取按照权重处理后的频率次数 算法可能不合理
     * @param $ip_cnt
     * @param $client_cnt
     * @return int
     */
    private function getRateByWeight($ip_cnt, $client_cnt)
    {
        if(empty($weight = $this->rule['rate']['client_weight'])) {
            return $ip_cnt;
        }

        if($weight == 100) {
            return $client_cnt;
        }

        return $client_cnt + ($ip_cnt - $client_cnt) * (1 - $weight/100);
    }

    /**
     * 输出block时的响应内容
     * @param string $type
     */
    private function render($type = 'black')
    {
        exit(empty($this->config['block_response'][$type]) ? '' : (string)$this->config['block_response'][$type]);
    }


    /**
     * @var \soa\firewall\model\access
     */
    private $accessInstance;

    /**
     * @return \soa\firewall\model\access
     */
    private function getAccessInstance()
    {
        if(empty($this->accessInstance)) {
            $this->accessInstance = new \soa\firewall\model\access();
        }
        return $this->accessInstance;
    }


    /**
     * @var \soa\firewall\model\rule
     */
    private $ruleInstance;

    /**
     * @return \soa\firewall\model\rule
     */
    private function getRuleInstance()
    {
        if(empty($this->ruleInstance)) {
            $this->ruleInstance = new \soa\firewall\model\rule($this->service);
        }
        return $this->ruleInstance;
    }

    /**
     * @return firewall
     */
    private static function getInstance()
    {
        if(empty(self::$instance)) {
            self::$instance = new firewall();
        }
        return self::$instance;
    }

}